<!--
	Copyright 2008 Google Inc.

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.

	@author Brad Neuberg

	The slurp method and implementation is taken from Brian Dunnington's
	Vortex library
	(http://geekycodestuff.blogspot.com/2007/09/vortexoffline-demo-app.html),
	which is itself taken from Brad Neuberg's Dojo Offline
	(http://dojotoolkit.org/offline).
	Both are under the BSD license.
-->

<html>
<head>
	<script>
	window.onload = function() {
		// write out the bookmarklet
		var a = document.getElementById('bookmarklet');
		a.href = toBookmarklet(bookmarklet);
	}

	/**
	Generates a bookmarklet that will inline this
	object and cause it to be run.
	*/
	function toBookmarklet(f) {

		// wrap as bookmarklet
		return 'javascript:(function(){function GoogleReaderOffline() { this.init(); } GoogleReaderOffline.prototype = '+ compressFunctions(googleReaderOfflinePrototypes) +';  function GoogleReaderPageUtils() { } GoogleReaderPageUtils.prototype = '+ compressFunctions(googleReaderPageUtilsPrototypes) +'; window.googleReaderPageUtils = new GoogleReaderPageUtils(); function LocalStorage() { this.init(); } LocalStorage.prototype = ' + compressFunctions(localStoragePrototypes) + '; var f = ' + compressFunctions(bookmarklet) + '; f.run(f);})()'
	}

	function compressFunctions(f) {
		// enumerate the functions
		var allFuncs = [];
		for (var i in f) {
			allFuncs.push(i + ': ' + f[i]);
		}

		// stringify them and put commas between them
		var s = allFuncs.join(',');

		// remove trailing comma
		if (s.charAt(s.length - 1) == ',') {
			s = s.substring(0, s.length - 2);
		}

		// remove new lines
		s = s.replace(/\n/g, '');

		// turn into an object literal
		s = '{' + s + '}';

		return s;

	}
 
	googleReaderOfflinePrototypes = {

		init: function() {
			this.localStorage = new LocalStorage();
			this.subscriptionsMap = new Object();
			googleReaderPageUtils.addOfflineControls();
		},

		goOffline: function() {
			this.captureHeaderButtons();
			this.captureClicks();
			this.getAuthToken();
			this.getSubscriptions();
		},

		/* Get a JSON stream representing the subscription information for the 
		the user.
		*/
		getSubscriptions: function() {
			var self = this;
			this.makeRequest("http://www.google.com/reader/api/0/subscription/list?output=json", true, function(request) {
				var subscriptions = request.responseText;
				this.unmarshallSubscriptions(eval('(' + subscriptions + ')'));
				this.getSubscriptionUnreadCounts();
				this.localStorage.saveUserSubscriptions(subscriptions);
				this.retrieveItems(
					function(current, numOfSubs){ 
						googleReaderPageUtils.setStatusCount(current, numOfSubs);
					}, 
					function(numOfSubs){
						googleReaderPageUtils.setStatus("");
						googleReaderPageUtils.preparePage();
						self.processSubscriptions();
						googleReaderPageUtils.setHeader("Subscriptions");
					});
				
			});
		},

		/* Fill in the unread counts for the subcriptions.
		*/
		getSubscriptionUnreadCounts: function() {
			this.makeRequest("http://www.google.com/reader/api/0/unread-count?all=true&output=json", false, function(request) {
				var unreadCounts = request.responseText;
				this.localStorage.saveUserUnreadSubscription(unreadCounts)
				var subscriptionsUnreadCountsJson = eval('(' +  unreadCounts + ')');
				if (subscriptionsUnreadCountsJson) {
					var subscriptionsUnreadCounts = subscriptionsUnreadCountsJson.unreadcounts;
					for (i=0; i < subscriptionsUnreadCounts.length; i++) {
						var subscription = this.subscriptionsMap[subscriptionsUnreadCounts[i].id];
						if (subscription) {
							subscription.unreadCount = subscriptionsUnreadCounts[i].count;
						}
					}
				}
			});
		},

		processItemsForStream: function(streamId) {
			var self = this;
			this.localStorage.extractItemsFromStream(streamId, function(results) {
				googleReaderPageUtils.clearPage();
				for (i = 0 ; i < results.rows.length; i++) {
					var row = results.rows.item(i);
					googleReaderPageUtils.appendContent(row['itemDocId'], streamId, row['title'], self.subscriptionsMap[streamId].title, 
														row['content'].replace( /<\S[^><]*>/g, "").substring(0,50)+"...");
				}
				googleReaderPageUtils.setHeader(self.subscriptionsMap[streamId].title);
				//FIXME
				self.captureClicks();
			});
		},

		processContentForItem: function(itemDocId, stream) {
			var self = this;
			this.localStorage.extractContentsForItem(itemDocId, function(results) {
				googleReaderPageUtils.clearPage();
				var resultRow = results.rows.item(0);
				googleReaderPageUtils.setContent(resultRow['content']);
				googleReaderPageUtils.setHeader(resultRow['title']);
				self.localStorage.markItemAsRead(itemDocId);
				self.markItemAsRead(itemDocId, stream);
				//FIXME
				self.captureClicks();
			});

		},

		makeRequest: function(url, sync, callBack) {
			var request = new XMLHttpRequest();
			request.open("GET", url, sync);
			var self = this;
			request.onreadystatechange = function() {
				if (request.readyState == 4) {
					if (callBack) {
						callBack.call(self, request);
					}
				}
			}
			request.send(null);
		},
		
		postRequest: function(url, sync, params, callBack) {
			var request = new XMLHttpRequest();
			request.open("POST", url, sync);
			var self = this;
			request.onreadystatechange = function() {
				if (request.readyState == 4) {
					if (callBack) {
						callBack.call(self, request);
					}
				}
			}
			request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");			
			request.send(params);
		},
		
		markItemAsRead: function(itemDocId, source) {
			var params = "T="+this.authToken+"&a=" + ("user/-/state/com.google/read") + "&async=true&ac=edit-tags&s="+ (source.substring(0,source.length-1)) +"&i="+ ("tag:google.com,2005:reader/item/")+itemDocId;
			//alert(params);
			this.postRequest("http://www.google.com/reader/api/0/edit-tag?client=scroll", true, params, function(request) {
				//alert(request.responseText);
			});
		},
		
		getAuthToken: function() {
			var self = this;
			this.makeRequest("http://www.google.com/reader/api/0/token?client=scroll", false, function(request) {
				self.authToken = request.responseText;
			});
			return this.authToken;
		},

		processSubscriptions: function() {
			googleReaderPageUtils.clearPage();
			var holdingDiv = document.getElementById("holding");
			for (sub in this.subscriptionsMap) {
				var subscription = this.subscriptionsMap[sub];
				var title = subscription['title'];
				var unreadCount = subscription['unreadCount'];
				if (unreadCount > 0) {
					var subLink = googleReaderPageUtils.createSubscriptionLink(sub, title, unreadCount);
					holdingDiv.appendChild(subLink);
				}
			}
			googleReaderPageUtils.setHeader("Subscriptions");
			this.captureClicks();
		},

		/* Go through the subscriptions and retrieve all the feeds that have unread items
		*/
		retrieveItems: function(perSubHandler, finalSubHandler) {
			var numOfSubsToGet = this.getNumOfActiveSubscriptions();
			var currentSubCount = 0;
			for (subId in this.subscriptionsMap) {
				var currentSubscription = this.subscriptionsMap[subId];
				if (currentSubscription.unreadCount > 0) {
					this.makeRequest("http://www.google.com/reader/api/0/stream/contents/"+subId+"?r=n&xt=user/-/state/com.google/read&output=json", true, function(request) {
						var feed = eval('(' + request.responseText + ')');
						if (feed) {
							
							currentSubCount++;
			
							// Call the perSubHandler
							if (perSubHandler) {
								perSubHandler.call(this, currentSubCount, numOfSubsToGet);
							}
							
							for (i = 0; i < feed.items.length ; i++) {
								currentItem = feed.items[i];
								var itemId = googleReaderPageUtils.itemCodeFromItemId(currentItem.id);
								var content;
								if (currentItem.summary) {
									content = currentItem.summary.content;
								} else {
									// Try the content of the content!
									if (currentItem.content) {
										content = currentItem.content.content;
									}
								}
								this.localStorage.saveItemsForSubscription(itemId, feed.id, currentItem.crawlTimeMsec, currentItem.crawlTimeMsec, 0, currentItem.title, currentItem.caregories, content);
								
							}
							// We have finished getting all of the items...
							if (currentSubCount == numOfSubsToGet) {
								if (finalSubHandler) {
									finalSubHandler.call(this, numOfSubsToGet);
								}
							}
						}
					});
				}
			}
		},
		
		getNumOfActiveSubscriptions: function() {
			var length = 0;
			for (sub in this.subscriptionsMap) {
				if (this.subscriptionsMap[sub].unreadCount > 0) {
					length++;
				}
			}
			return length;
		},
		
		/* Store each of the subscriptions in an instance cache convinience 
		*/
		unmarshallSubscriptions: function(jsonOfSubscriptions) {
			var subscriptions = jsonOfSubscriptions.subscriptions;
			for (i = 0; i < subscriptions.length; i++) {
				this.subscriptionsMap[subscriptions[i].id] = subscriptions[i];
			}
		},

		captureClicks: function() {
			var self = this;
			googleReaderPageUtils.registerForAllLinksOfPattern("feed.*en=el", function(event) {
				var stream = googleReaderPageUtils.extractFeedFromLink(event.currentTarget.href);	
				self.processItemsForStream(stream);
			});
			googleReaderPageUtils.registerForAllLinksOfPattern("feed.*i=", function(event) {
				var itemDocId = googleReaderPageUtils.extractItemDocIdFromLink(event.currentTarget.href);
				var stream = googleReaderPageUtils.extractFeedFromLink(event.currentTarget.href);
				self.processContentForItem(itemDocId, stream);
			});
		},

		captureHeaderButtons: function() {
			var self = this;
			googleReaderPageUtils.registerForAllLinksOfPattern("tags", function(event) {
				alert("tags");
			});
			googleReaderPageUtils.registerForAllLinksOfPattern("subscriptions", function(event) {
				self.processSubscriptions();
			});
		}
	}
	
	googleReaderPageUtilsPrototypes = {

		/* Adds a new url hander. When a url that matches the urlPattern is clicked on
		* the handler is invoked.
		*/
		registerClickHandler: function(element, handler) {
			element.addEventListener('click', function(event) {
				event.stopPropagation();
				event.preventDefault();
				if (handler) {
					handler.call(this, event);
				}
				}, true);
		},

		registerForAllLinksOfPattern: function(urlPattern, handler) {
			var links = this.getLinksWithPattern(urlPattern);
			for (i = 0; i < links.length; i++) {
				this.registerClickHandler(links[i], handler);
			}
		},

		getLinksWithPattern: function(urlPattern) {
			var links = new Array();
			elements = document.getElementsByTagName('a');
			for (i = 0; i < elements.length; i++) {
				var element = elements[i];
				if (element.href.match(urlPattern)) {
					links.push(element);
				}
			}
			return links;
		},
		
		extractFeedFromLink: function(linkLocation) {
			var startOfFeed = linkLocation.indexOf("feed");
			return unescape(linkLocation.substring(startOfFeed, linkLocation.indexOf("?")));
		},

		extractItemDocIdFromLink: function(linkLocation) {
			var startOfFeed = linkLocation.indexOf("i=");
			return unescape(linkLocation.substring(startOfFeed+2));
		},


		itemCodeFromItemId: function(itemId) {
			var lastSlash = itemId.lastIndexOf("/");
			return itemId.substring(lastSlash+1);
		},

		extractFeedItem: function(page) {
			var bodyIndex = page.indexOf("<div class=\"header\">"); 
			var endBodyIndex = page.indexOf("<div class=\"entry-next\">"); 
			return page.slice(bodyIndex, endBodyIndex);
		},

		getElementsByClassName: function(className) {
			var xpathResult = document.evaluate("//*[@class = '"+ className + "']", document, null, 0, null);
			var outArray = new Array();
			var item;
			while (item = xpathResult.iterateNext()) {
				outArray[outArray.length] = item;
			}
			return outArray;
		},
		
		getBottomNavBar: function() {
			var bottomLinks = this.getElementsByClassName("bottom-nav");
			if (bottomLinks.length == 0) {
				bottomLinks = this.getElementsByClassName("bottom-nav-isolated");
			}
			return bottomLinks[0];
		},
		
		addOfflineControls: function() {
			var bottomLinks = this.getBottomNavBar();
			var controlsDiv = document.createElement('span');
			controlsDiv.id = "offlineControls";
			controlsDiv.innerHTML = "&nbsp;|&nbsp;<a href='javascript:googleReaderOffline.goOffline()'>Offline</a>"
			bottomLinks.appendChild(controlsDiv);
			var statusDiv = document.createElement('span');
			statusDiv.id = "statusControl"
			bottomLinks.appendChild(statusDiv);
		},
		
		setStatusCount: function(current, max) {
			this.setStatus("&nbsp;|&nbsp Downloading:"+current + " / " + max);
		},
		
		setStatus: function(status) {
			var statusDiv = document.getElementById('statusControl');
			statusDiv.innerHTML = status;
		},

		preparePage: function() {
			var items = this.getElementsByClassName("sub-link");
			for (i = 0; i < items.length; i++) {
				items[i].parentNode.removeChild(items[i]);
			}
			// We now insert a <div> which will be a holder for our content
			var holdingDiv = document.createElement('div');
			holdingDiv.id = "holding";
			var footer = this.getBottomNavBar();
			if (footer) {
				footer.parentNode.insertBefore(holdingDiv, footer);
			}
		},

		clearPage: function() {
			var  holdingDiv = document.getElementById('holding');
			holdingDiv.innerHTML = "";
		},
		
		setHeader: function(headerText) {
			var header = googleReaderPageUtils.getElementsByClassName("header")[0];
			header.innerHTML = headerText;
		},
		
		setContent: function(content) {
			var holdingDiv = document.getElementById("holding");
			holdingDiv.innerHTML = content;
		},
		
		appendContent: function(itemDocId, streamId, title, source, summary) {
			var holdingDiv = document.getElementById("holding");
			holdingDiv.appendChild(googleReaderPageUtils.createItemLink(itemDocId, streamId, title, source, summary));
		},
		
		/* I create a link to a stored item 
		*/
		createItemLink: function(itemDocId, streamId, title, source, summary)  {
			var link = document.createElement('a');
			link.href = "http://www.google.com/reader/m/view/"+streamId+"/?i="+itemDocId;
			link.innerHTML = "<div class=\"row whisper\"><div class=\"text\">" +"<span class=\"item-title\">" + title + "</span>"  + "<span class=\"item-source\"> - " + source + "</span><br>" + summary + "</div></div>";
			return link;
		},

		createSubscriptionLink: function(streamId, title, unreadCount) {
			var link = document.createElement('a');
			link.href = "http://www.google.com/reader/m/view/"+streamId+"?en=el";
			link.innerHTML = "<div class=\"row whisper sub\"><div class=\"text sub-item unread-sub\">"+ title +"</div><span class=\"item-count larger whisper\">"+ unreadCount +"</span></div>";
			return link;
		}

	}

	localStoragePrototypes =  {
		init: function() {
			if (window.openDatabase) {
				this.db = openDatabase("GoogleReaderOffline", "1.0", "Offline Mode test", 200000);
				if (!this.db) {
					alert("Failed to open the database on disk.");
				}
			} else {
				alert("Couldn't open the database.  Please try with a WebKit nightly with this feature enabled");
			}
			//this.runSQL("CREATE TABLE items (itemDocId TEXT PRIMARY KEY NOT NULL, sourceStreamId TEXT NOT NULL, timestampMsec INTEGER NOT NULL, timestampScrubber INTEGER NOT NULL, isRead INTEGER NOT NULL, title TEXT NOT NULL, categories TEXT NOT NULL, content TEXT NOT NULL)",[]);
			//this.runSQL("CREATE TABLE streams (itemDocId TEXT NOT NULL, streamId TEXT NOT NULL, PRIMARY KEY (itemDocId, streamId))", []);
			//this.runSQL("CREATE TABLE userdata (id undefined NOT NULL, json undefined NOT NULL, PRIMARY KEY (id))",[]);
			this.runSQL("DELETE FROM items",[]);
			this.runSQL("DELETE FROM streams", []);
			this.runSQL("DELETE FROM userdata",[]);

		},

		saveUserSubscriptions: function(subscriptions) {
			this.runSQL("REPLACE INTO userdata (id, json) VALUES (?, ?)", ["subscriptions", subscriptions]);
		},

		saveUserUnreadSubscription: function(unreadsubscriptions) {
			this.runSQL("REPLACE INTO userdata (id, json) VALUES (?, ?)", ["unreadcounts", unreadsubscriptions]);
		},

		extractItemsFromStream: function(streamId, handler) {
			this.runSQL("SELECT items.itemDocId, title, content from items, streams where streams.streamId = '"+ streamId +"' and streams.itemDocId = items.itemDocId and isRead = 0", [], handler);
		},

		markItemAsRead: function(itemDocId) {
			this.runSQL("UPDATE items set isRead = 1 where itemDocId = '"+itemDocId+"'", []);
		},

		extractContentsForItem: function(itemDocId, handler) {
			this.runSQL("SELECT * from items where itemDocId='"+itemDocId+"'", [], handler);
		},

		saveItemsForSubscription: function(itemId, subscription, timestampMsec, timestampScrubber, isRead, title, categories, content) {			
			this.runSQL("REPLACE INTO items(itemDocId, sourceStreamId, timestampMsec, timestampScrubber, isRead, title, categories, content) VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
			[itemId, subscription, timestampMsec, timestampScrubber, 0,title, categories, content]);
			this.runSQL("REPLACE INTO streams(itemDocId, streamId) VALUES(?, ?)",[itemId, subscription]);
		},

		runSQL: function(sql, params, callBack) {
			this.db.transaction(function(tx) {
				tx.executeSql(sql, params, 
					function(tx, result) {
						if (callBack) {
							callBack.call(this, result);
						}
					}, 
					function(tx, error) {
						alert("Problem running SQL"+error.message);
					}	
				);
			}, function(error) {
				alert("Problem running SQL"+error.message);
			});
		}

	}

	/**
	We stringify this object into a bookmarklet. Doing it in 
	this form rather than a javascript: URL makes it much more
	maintainable and easier to read.
	*/
	var bookmarklet = {
		/** 
		For testing -- mock value for window.location.
		Must have same properties as window.location, such as .href,
		.protocol, etc.
		*/
		testLocation_: null,

		/**
		External entry point to actually execute this
		bookmarklet. 
		*/
		run: function(self) {
			window.googleReaderOffline = new GoogleReaderOffline();

			//self.clickHandlers_ = new Object();

			//self.registerClickHandler_(self, ".*feed(.*)?", function(matchedItems) { alert("Clicked"); })
			//self.captureHeaderButtons_(self);
			//self.captureClicks_(self);
			//self.localStorage = new LocalStorage();
			//self.getSubscriptions_(self);
			//self.preparePage_(self);

		}

	}

		/*
		Print debug/log messages on browsers without Firebug. 
		*/
		if (typeof console == 'undefined' || !console.debug) {
			console = {};
			console.debug = function(msg) {
				var e = document.createElement('div');
				e.style.backgroundColor = 'white';
				e.style.color = 'black';
				e.appendChild(document.createTextNode(msg));
				var body = document.getElementsByTagName('body');
				if (!body || !body.length || body.length == 0) {
					return;
				}
				body[0].appendChild(e);
			}
		}
		</script>
	</head>

	<body>
		<h1>Manifest Bookmarklet</h1>

		<p>This page contains a bookmarklet that makes it easy to
			create Gears manifest files without having to write them by hand. It
			will automatically identify all the resources you want to make available
			offline, such as images, stylesheets, etc. and then generate a
			manifest file for you.</p>

			<p>To use, drag the following bookmarklet to your links toolbar. Then, 
				navigate to the web page that you would like to offline enable. Once
				pressed, this bookmarklet will scan the page, identify all the resources
				that you might want to have offline, and automatically generate
				a Gears offline manifest file for you. This manifest file will 
				appear in a text area on your screen -- you can now edit this file, then
				copy and save it into a new file, such as <i>manifest.js</i>.</p>

				<p>Note that this script only works on Firefox and Safari due to
					limitations in Internet Explorer on the length of bookmarklets.</p>

					<div><a id="bookmarklet" href="#">Generate Gears Manifest</a></div> 
				</body>
				</html>